"""
Regression demo for bike-share dataset using the NeuralNetwork class.

This script demonstrates regression for predicting casual and registered bike rentals
using a neural network with multiple hidden layers. The demo includes comprehensive
data preprocessing and evaluation metrics.

Key Performance Metrics:
- MSE (Mean Squared Error): Average of squared differences between predictions and actual values.
- RMSE (Root Mean Squared Error): Square root of MSE, giving a measure in the same units as the target.
  RMSE represents the standard deviation of prediction errors (residuals). A lower RMSE indicates
  better fit, with the value indicating the typical magnitude of prediction error. For example,
  an RMSE of ~89 means predictions are typically off by about 89 bikes.

To run this demo, use:
    python -m demos.bike_share.demo-bike-share
from the project root directory.
"""
import numpy as np
import csv
from datetime import datetime
import random
import os

# Import the neural network components from the new package
from neural_network import Layer
from neural_network import ReLUActivation
from neural_network import SigmoidActivation
from neural_network import TanhActivation
from neural_network import LeakyReLUActivation
from neural_network import NeuralNetwork
from neural_network import LossMSE

# Settings
hidden_sizes = [128]*4
activation_hidden = LeakyReLUActivation
epochs = 10000
learning_rate = 1e-3
batch_size = 64
test_split = 0.2

# Load data
print("Loading bike-share dataset from local CSV file...")
# Use package-relative path to the data file
data_path = os.path.join(os.path.dirname(__file__), 'bike-share-data.csv')
with open(data_path, 'r') as f:
    reader = csv.DictReader(f)
    data_rows = list(reader)

# Shuffle and split
random.seed(0)
random.shuffle(data_rows)
n_total = len(data_rows)
n_train = int(n_total * (1 - test_split))
train_rows = data_rows[:n_train]
test_rows = data_rows[n_train:]

# Feature extraction
def extract_datetime_features(rows):
    hours, dows, months = [], [], []
    for r in rows:
        dt = datetime.strptime(r['datetime'], '%Y-%m-%d %H:%M:%S')
        hours.append(dt.hour)
        dows.append(dt.weekday())
        months.append(dt.month)
    return np.array(hours), np.array(dows), np.array(months)

def cyclical_encode(arr, period):
    sin_vals = np.sin(2 * np.pi * arr / period)
    cos_vals = np.cos(2 * np.pi * arr / period)
    return sin_vals, cos_vals

# Preprocess train features
h_train, dow_train, m_train = extract_datetime_features(train_rows)
h_sin_train, h_cos_train = cyclical_encode(h_train, 24)
dow_sin_train, dow_cos_train = cyclical_encode(dow_train, 7)
m_sin_train, m_cos_train = cyclical_encode(m_train, 12)

# One-hot categorical (season, holiday, workingday, weather)
n = n_train
season_idx = np.array([int(r['season'])-1 for r in train_rows])
season_oh = np.zeros((4, n)); season_oh[season_idx, np.arange(n)] = 1
holiday_idx = np.array([int(r['holiday']) for r in train_rows])
holiday_oh = np.zeros((2, n)); holiday_oh[holiday_idx, np.arange(n)] = 1
working_idx = np.array([int(r['workingday']) for r in train_rows])
working_oh = np.zeros((2, n)); working_oh[working_idx, np.arange(n)] = 1
weather_idx = np.array([int(r['weather'])-1 for r in train_rows])
weather_oh = np.zeros((4, n)); weather_oh[weather_idx, np.arange(n)] = 1

# Continuous features and standardization
temp_train = np.array([float(r['temp']) for r in train_rows])
atemp_train = np.array([float(r['atemp']) for r in train_rows])
humidity_train = np.array([float(r['humidity']) for r in train_rows])
wind_train = np.array([float(r['windspeed']) for r in train_rows])

cont_stack = np.vstack([temp_train, atemp_train, humidity_train, wind_train])
# Add a small epsilon to std to prevent division by zero
mean = np.mean(cont_stack, axis=1, keepdims=True)
std = np.std(cont_stack, axis=1, keepdims=True) + 1e-8
cont_train_std = (cont_stack - mean) / std

# Assemble X_train
X_train = np.vstack([
    h_sin_train, h_cos_train,
    dow_sin_train, dow_cos_train,
    m_sin_train, m_cos_train,
    season_oh,
    holiday_oh,
    working_oh,
    weather_oh,
    cont_train_std
])

# Targets
y_train = np.vstack([
    np.array([int(r['casual']) for r in train_rows]),
    np.array([int(r['registered']) for r in train_rows])
])

# Standardize targets per output using train stats so MSE and its
# gradient stay well-scaled (raw counts can be in the hundreds).
y_mean = np.mean(y_train, axis=1, keepdims=True)
y_std  = np.std(y_train,  axis=1, keepdims=True) + 1e-8
y_train_std = (y_train - y_mean) / y_std

# Preprocess test features using train stats
h_test, dow_test, m_test = extract_datetime_features(test_rows)
h_sin_test, h_cos_test = cyclical_encode(h_test, 24)
dow_sin_test, dow_cos_test = cyclical_encode(dow_test, 7)
m_sin_test, m_cos_test = cyclical_encode(m_test, 12)

n_test = len(test_rows)
season_idx_t = np.array([int(r['season'])-1 for r in test_rows])
season_oh_t = np.zeros((4, n_test)); season_oh_t[season_idx_t, np.arange(n_test)] = 1
holiday_idx_t = np.array([int(r['holiday']) for r in test_rows])
holiday_oh_t = np.zeros((2, n_test)); holiday_oh_t[holiday_idx_t, np.arange(n_test)] = 1
working_idx_t = np.array([int(r['workingday']) for r in test_rows])
working_oh_t = np.zeros((2, n_test)); working_oh_t[working_idx_t, np.arange(n_test)] = 1
weather_idx_t = np.array([int(r['weather'])-1 for r in test_rows])
weather_oh_t = np.zeros((4, n_test)); weather_oh_t[weather_idx_t, np.arange(n_test)] = 1

temp_test = np.array([float(r['temp']) for r in test_rows])
atemp_test = np.array([float(r['atemp']) for r in test_rows])
humidity_test = np.array([float(r['humidity']) for r in test_rows])
wind_test = np.array([float(r['windspeed']) for r in test_rows])

cont_stack_t = np.vstack([temp_test, atemp_test, humidity_test, wind_test])
cont_test_std = (cont_stack_t - mean) / std

X_test = np.vstack([
    h_sin_test, h_cos_test,
    dow_sin_test, dow_cos_test,
    m_sin_test, m_cos_test,
    season_oh_t,
    holiday_oh_t,
    working_oh_t,
    weather_oh_t,
    cont_test_std
])

y_test = np.vstack([
    np.array([int(r['casual']) for r in test_rows]),
    np.array([int(r['registered']) for r in test_rows])
])
y_test_std = (y_test - y_mean) / y_std

# Build the network
input_size = X_train.shape[0]
network = NeuralNetwork()
prev_size = input_size
for size in hidden_sizes:
    network.add_layer(Layer(prev_size, size))
    network.add_layer(activation_hidden())
    prev_size = size
network.add_layer(Layer(prev_size, 2))

# Loss function
loss_obj = LossMSE()

# Training the neural network
print("Training the neural network...")

network.train(
    X_train, y_train_std,
    loss_obj,
    epochs,
    learning_rate,
    batch_size,
    shuffle=True,
    verbose=True,
    X_val=X_test,
    y_val=y_test_std,
    metric="rmse",
    target_std=y_std,
    plot=True,
)

# Get final predictions and un-scale back to real bike counts.
y_pred_test_std = network.forward_pass(X_test)
y_pred_test = y_pred_test_std * y_std + y_mean

print("\nTraining complete. Displaying predictions for 10 test samples:")

# Display predictions for 10 test samples
num_samples_to_show = 10
for i in range(num_samples_to_show):
    # Get original test row data
    original_row = test_rows[i]
    datetime_str = original_row['datetime']
    actual_casual = int(original_row['casual'])
    actual_registered = int(original_row['registered'])
    actual_total = int(original_row['count'])

    # Get predicted values
    predicted_values = y_pred_test[:, i]
    predicted_casual = predicted_values[0]
    predicted_registered = predicted_values[1]
    predicted_total = predicted_casual + predicted_registered

    # Get weather conditions
    season = int(original_row['season'])
    holiday = int(original_row['holiday'])
    workingday = int(original_row['workingday'])
    weather = int(original_row['weather'])
    temp = float(original_row['temp'])
    humidity = float(original_row['humidity'])
    windspeed = float(original_row['windspeed'])
    
    # Display conditions and predictions
    print(f"\nSample {i+1}: {datetime_str}")
    print(f"  Conditions: Season={season}, Weather={weather}, Temp={temp}°C, Humidity={humidity}%, Wind={windspeed}")
    print(f"  Holiday={holiday}, WorkingDay={workingday}")
    print(f"  Actual: Casual={actual_casual}, Registered={actual_registered}, Total={actual_total}")
    print(f"  Predicted: Casual={predicted_casual:.2f}, Registered={predicted_registered:.2f}, Total={predicted_total:.2f}")

